/*
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 *
 * The Initial Developer of the Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 * Portions created by Mark A. Kobold are Copyright (C) 2000-2007. All Rights Reserved.
 *
 * Contributor(s): 
 *  Mark A. Kobold [mkobold <at> isqlviewer <dot> com].
 *  
 * If you didn't download this code from the following link, you should check
 * if you aren't using an obsolete version: http://www.isqlviewer.com
 */
package org.isqlviewer.bookmarks;

import java.io.Serializable;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Vector;

import org.isqlviewer.util.BasicUtilities;
import org.isqlviewer.util.LocalMessages;

/**
 * Folder component for representing a collection of bookmarks and bookmark folders.
 * <p>
 * This object is the basis of creating a bookmark tree. This component is modeled loosly after the java.io.File object
 * since bookmarks are organized in similar fashion to files.
 * 
 * @author Mark A. Kobold &lt;mkobold at isqlviewer dot com&gt;
 * @version 1.0
 */
public class BookmarkFolder implements Serializable {

    private static final long serialVersionUID = -5334474850329646918L;
    /**
     * This bookmark folder is the root of the folder tree, and this folder has no parent.
     * <p>
     * 
     * @see #getType()
     */
    public static final int TYPE_ROOT = 0;
    /**
     * This bookmark folder is folder that has dynamic contents based on other variables.
     * <p>
     * 
     * @see #getType()
     */
    public static final int TYPE_SMART = 1;
    /**
     * This bookmark folder is the regular folder that has no special function.
     * <p>
     * 
     * @see #getType()
     */
    public static final int TYPE_NORMAL = 2;

    private static final String RESOURCE_BUNDLE = "org.isqlviewer.bookmarks.ResourceBundle";
    private static final LocalMessages messages = new LocalMessages(RESOURCE_BUNDLE);
    // sorter that ensures that folders are first then bookmarks, both are sorted lexigraphically by name.
    private transient final Comparator<Object> sorter = new FolderSorter();
    // one of the specific TYPE_XXX for the purpose of this folder.
    private int type = TYPE_NORMAL;
    // user specified name for this folder.
    private String name = null;
    // parent folder should only be null if the type is root and vice-versa.
    private BookmarkFolder parentFolder = null;
    // collection of child elements it is mix of Bookmarks and Bookmark Folders.
    private Vector<Object> children = new Vector<Object>();
    // path name for this folder.
    private String path = null;

    /**
     * Creates a new root folder for creating a new bookmark tree.
     * <p>
     * 
     * @return new bookmark folder that has a type of {@link #TYPE_ROOT}.
     */
    public static BookmarkFolder createRootFolder() {

        return new BookmarkFolder("/", TYPE_ROOT);
    }

    public static BookmarkFolder[] getPathElements(BookmarkFolder folder) {

        Vector<BookmarkFolder> elements = new Vector<BookmarkFolder>();
        elements.add(folder);
        BookmarkFolder parentFolder = folder;
        while ((parentFolder = parentFolder.parentFolder) != null) {
            elements.insertElementAt(parentFolder, 0);
        }
        return elements.toArray(new BookmarkFolder[elements.size()]);
    }

    /**
     * Creates a new bookmark using the specified parent.
     * <p>
     * The folder created by this method will already be added to the parent folder.
     * 
     * @param folderName of this new folder.
     * @throws IllegalArgumentException is null parentFolder is given.
     */
    public BookmarkFolder(String folderName) {

        this(folderName, TYPE_NORMAL);
    }

    /**
     * Constructor for creating new instance of a bookmark.
     * <p>
     * 
     * @param folder parent folder for this folder instance if any.
     * @param type the type of folder it actually is.
     * @throws IllegalArgumentException if the type is not root and foler is null.
     */
    BookmarkFolder(String folderName, int type) {

        if (type != TYPE_ROOT && folderName == null) {
            throw new IllegalArgumentException(messages.format(""));
        }
        this.name = folderName;
        this.type = type;
        calculatePath();
    }

    /**
     * Constructor for creating new instance of a bookmark.
     * <p>
     * 
     * @param folder parent folder for this folder instance if any.
     * @param type the type of folder it actually is.
     * @throws IllegalArgumentException if the type is not root and foler is null.
     */
    BookmarkFolder(BookmarkFolder parentFolder, String folderName) {

        if (type != TYPE_ROOT && folderName == null) {
            throw new IllegalArgumentException(messages.format(""));
        }
        this.name = folderName;
        this.parentFolder = parentFolder;
        calculatePath();
    }

    @Override
    /**
     * Returns the full path of this bookmark folder.
     * <p>
     * 
     * @return path for this folder.
     */
    public String toString() {

        return name;
    }

    /**
     * Gets the user-specific name for this folder.
     * <p>
     * This value can be null if no name has been set. if this folder is a root folder a simple '/' will be returned.
     * 
     * @return returns the name of this folder.
     * @see #setName(String)
     */
    public String getName() {

        return type == TYPE_ROOT ? path : name;
    }

    /**
     * Gets the name of the parent folder for this folder.
     * <p>
     * If this is a root folder than the folder will null.
     * 
     * @return name of the parent folder.
     */
    public String getParent() {

        return type == TYPE_ROOT ? null : parentFolder.getName();
    }

    /**
     * Gets the full path of this folder.
     * <p>
     * The path will be in a *NIX style convention of '/parent/child/folder/'
     * 
     * @return the path represented by this folder.
     */
    public String getPath() {

        return path;
    }

    /**
     * Returns a string list of all the names of the folders and bookmarks in this folder.
     * <p>
     * This will return a list of all folders and bookmark names contained in this folder. All folders will be first in
     * the list and will be prefixed with '/'.
     * 
     * @return string list of the names of all components in this folder.
     */
    public String[] list() {

        synchronized (children) {
            String[] names = new String[children.size()];
            for (int i = 0; i < names.length; i++) {
                Object o = children.get(i);
                if (o instanceof BookmarkFolder) {
                    names[i] = '/' + ((BookmarkFolder) o).name;
                } else if (o instanceof Bookmark) {
                    names[i] = ((Bookmark) o).getName();
                }
            }
            return names;
        }
    }

    /**
     * Searches for a folder using the specified path.
     * <p>
     * Assumes that the given path precedes the given path.
     * 
     * @param childPath to search for within this folder.
     * @return folder based on the path, can be <tt>null</tt> if child path does not exist.
     */
    public BookmarkFolder findChildPath(String childPath) {

        if (childPath == null || childPath.trim().length() == 0) {
            return null;
        }

        synchronized (children) {
            String childName = null;
            String subPath = null;
            if (childPath.charAt(0) == '/') {
                int next = childPath.indexOf('/', 1);
                if (next < 0) {
                    childName = childPath.substring(1);
                    subPath = childName;
                } else {
                    childName = childPath.substring(1, next);
                    subPath = childPath.substring(next);
                }
            } else {
                childName = childPath.substring(0, childPath.indexOf('/', 0));
                subPath = childPath.substring(childName.length());
            }
            if (childName.trim().length() == 0) {
                return this;
            }
            for (int i = 0; i < children.size(); i++) {
                Object o = children.get(i);
                if (o instanceof BookmarkFolder) {
                    if (childName.equals(((BookmarkFolder) o).name)) {
                        if (subPath.indexOf('/') >= 0) {
                            return ((BookmarkFolder) o).findChildPath(subPath);
                        }
                        return (BookmarkFolder) o;
                    }
                }
            }
            return null;
        }
    }

    /**
     * @param string
     * @return
     */
    public BookmarkFolder mkdirs(String childPath) {

        if (childPath == null || childPath.trim().length() == 0) {
            return null;
        } else if (path.equalsIgnoreCase(childPath)) {
            return this;
        }

        synchronized (children) {
            String childName = null;
            String subPath = null;
            if (childPath.charAt(0) == '/') {
                int next = childPath.indexOf('/', 1);
                if (next < 0) {
                    childName = childPath.substring(1);
                    subPath = childName;
                } else {
                    childName = childPath.substring(1, next);
                    subPath = childPath.substring(next);
                }
            } else {
                int slashIndex = childPath.indexOf('/', 0);
                if (slashIndex >= 0) {
                    childName = childPath.substring(0, slashIndex);
                    subPath = childPath.substring(childName.length());
                } else {
                    childName = "";
                    subPath = "";
                }
            }

            if (childName.trim().length() == 0) {
                return this;
            }

            for (int i = 0; i < children.size(); i++) {
                Object o = children.get(i);
                if (o instanceof BookmarkFolder) {
                    if (childName.equals(((BookmarkFolder) o).name)) {
                        if (subPath.indexOf('/') >= 0) {
                            return ((BookmarkFolder) o).mkdirs(subPath);
                        }
                        return (BookmarkFolder) o;
                    }
                }
            }
            BookmarkFolder childFolder = addChildFolder(childName);
            return childFolder.mkdirs(subPath);
        }
    }

    /**
     * Changes the name of this folder.
     * <p>
     * Once the name is changed the path name will also be changed.
     * 
     * @param name of the folder to be set.
     * @throws IllegalStateException if the type of folder is ROOT
     * @throws IllegalArgumentException if the new name exists in the parent folder.
     */
    public void setName(String name) {

        if (type == TYPE_ROOT) {
            throw new IllegalStateException(messages.format("BookmarkFolder.RootRenameError"));
        }
        this.name = name;
        calculatePath();
    }

    /**
     * Gets the parent folder this folder was created with.
     * <p>
     * 
     * @return the parent folder of this instance; will be null if this is ROOT folder.
     */
    public BookmarkFolder getParentFolder() {

        return parentFolder;
    }

    /**
     * Gets the type of folder this instance is.
     * <p>
     * 
     * @return one of the TYPE_XXX constant values defined in this class.
     * @see #TYPE_NORMAL
     * @see #TYPE_ROOT
     * @see #TYPE_SMART
     */
    public int getType() {

        return type;
    }

    /**
     * Adds a child folder to this instance.
     * <p>
     * This method will essentially move the folder from another tree the child folder will be detached from existing
     * parent and added to this parent.
     * <p>
     * An illegal argument exception will be thrown if one of the following restrictions are broken.
     * <ul>
     * <li>TYPE_NORMAL folders can only have other TYPE_NORMAL sub-folders</li>
     * <li>TYPE_SMART folders can have TYPE_NORMAL or TYPE_SMART sub-folders</li>
     * <li>TYPE_ROOT folders can have TYPE_NORMAL or TYPE_SMART sub-folders</li>
     * </ul>
     * 
     * @param childFolder to add to this instance.
     * @throws NullPointerException if the child folder is null.
     * @throws IllegalArgumentException if child folder has type that is logicall incorrect for this folder.
     */
    public BookmarkFolder addChildFolder(String folderName) {

        if (folderName == null) {
            throw new NullPointerException(messages.format("BookmarkFolder.NullChildAddError", name));
        }
        synchronized (this) {
            BookmarkFolder childFolder = new BookmarkFolder(this, folderName);
            children.add(childFolder);
            BasicUtilities.sortCollection(children, sorter);
            return childFolder;
        }
    }

    /**
     * Removes a child folder from this instance.
     * <p>
     * Unlike the remove bookmark method this method cannot be called anywhere and the given folder must exist in this
     * instance.
     * <p>
     * If a <tt>null</tt> bookmark is given this method immediately returns <tt>false</tt>.
     * 
     * @param folder to remove from this instance.
     * @return <tt>true</tt> if the child folder was successfully removed from this instance.
     */
    public boolean removeFolder(BookmarkFolder folder) {

        if (folder == null) {
            return false;
        }

        synchronized (this) {
            boolean wasRemoved = children.remove(folder);
            if (wasRemoved) {
                BasicUtilities.sortCollection(children, sorter);
            }
            return wasRemoved;
        }
    }

    /**
     * Adds a bookmark to this folder.
     * <p>
     * The previous folder instance will no longer have the instance of the given bookmark after call.
     * 
     * @param bookmark to be added to this folder.
     * @throws NullPointerException if the child bookmark is null.
     */
    public void addBookmark(BookmarkReference bookmark) {

        if (bookmark == null) {
            throw new NullPointerException(messages.format("BookmarkFolder.NullChildAddError", name));
        }

        synchronized (bookmark) {
            BookmarkFolder folder = bookmark.getFolder();
            if (folder != null) {
                synchronized (folder) {
                    folder.removeChild(bookmark);
                }
            }
            synchronized (this) {
                children.add(bookmark);
                BasicUtilities.sortCollection(children, sorter);
                bookmark.setFolder(this);
            }
        }
    }

    /**
     * Removes a bookmark from the folder it resides within.
     * <p>
     * This method can technically be called on any folder, so the bookmark does not have to actually exist in this
     * folder instance to be removed from its parent folder.
     * <p>
     * If a <tt>null</tt> bookmark is given this method immediately returns <tt>false</tt>.
     * 
     * @param bookmark to remove from its parent folder.
     * @return <tt>true</tt> if the bookmark was successfully removed from the folder.
     */
    public boolean removeBookmark(BookmarkReference bookmark) {

        if (bookmark == null) {
            return false;
        }

        BookmarkFolder folder = bookmark.getFolder();
        synchronized (folder) {
            return folder.removeChild(bookmark);
        }
    }

    /**
     * Returns the index of the child component.
     * <p>
     * This method is here to mainly support the use of this object within the Swing TreeModel.
     * 
     * @param child to get the index of.
     * @return index of the child in the list of components; -1 if non-existent.
     * @see javax.swing.tree.TreeModel#getIndexOfChild(java.lang.Object, java.lang.Object)
     */
    public int indexOfChild(Object child) {

        return children.indexOf(child);
    }

    /**
     * Gets the number of child components that exist within this instance.
     * <p>
     * This method is here to mainly support the use of this object within the Swing TreeModel.
     * 
     * @return number of sub-folders and bookmarks that exist in this folder.
     * @see javax.swing.tree.TreeModel#getChildCount(java.lang.Object)
     */
    public int getChildCount() {

        return children.size();
    }

    /**
     * Enumeration for processing all child elements of this folder.
     * <p>
     * All objects return will be an instance of BookmarkFolder or Bookmark.
     * 
     * @return enumeration of elements contained in this folder.
     */
    public Enumeration<Object> childElements() {

        return children.elements();
    }

    /**
     * Gets the child component by index.
     * <p>
     * This method is here to mainly support the use of this object within the Swing TreeModel.
     * 
     * @param index of the child component to get.
     * @return Bookmark or BookmarkFolder at the specified index, can be null if non-existent.
     * @see javax.swing.tree.TreeModel#getChild(java.lang.Object, int)
     */
    public Object getChild(int index) {

        return children.get(index);
    }

    // removes the child component from this folder.
    private boolean removeChild(Object child) {

        return children.remove(child);
    }

    // quick patch calculation based on the path of the parent.
    private void calculatePath() {

        StringBuffer pathBuffer = new StringBuffer("");
        if (parentFolder != null) {
            pathBuffer.append(parentFolder.path);
            pathBuffer.append(name);
            pathBuffer.append("/");
        } else {
            pathBuffer.append("/");
        }

        path = pathBuffer.toString();
        pathBuffer.setLength(0);
        pathBuffer = null;
    }

    /**
     * Support comparator for ensuring that child elements of a folder are sorted correctly.
     * <p>
     * All child elements are sorted with bookmark-folders first then bookmarks. if two elements are of the same type
     * then they are organized by the names of the respective objects.
     * 
     * @author Mark A. Kobold &lt;mkobold at isqlviewer dot com&gt;
     * @version 1.0
     */
    private static class FolderSorter implements Comparator<Object> {

        public int compare(Object o1, Object o2) {

            Class c1 = o1 == null ? Object.class : o1.getClass();
            Class c2 = o2 == null ? Object.class : o2.getClass();
            if (c1.equals(c2) && o1 != null & o2 != null) {
                if (o1 instanceof BookmarkFolder) {
                    BookmarkFolder bf1 = (BookmarkFolder) o1;
                    BookmarkFolder bf2 = (BookmarkFolder) o2;
                    String n1 = bf1.name == null ? "" : bf1.name;
                    String n2 = bf2.name == null ? "" : bf2.name;
                    return n1.compareToIgnoreCase(n2);
                } else if (o1 instanceof BookmarkReference) {
                    BookmarkReference b1 = (BookmarkReference) o1;
                    BookmarkReference b2 = (BookmarkReference) o2;
                    return b1.getName().compareToIgnoreCase(b2.getName());
                }
            } else if (o1 != null && o2 != null) {
                // not null but different types sort folder first //
                return o1 instanceof BookmarkFolder ? -1 : 1;
            }
            return 0;
        }
    }

    /**
     * @param text
     * @return
     */
    public boolean containsBookmark(String bookmarkName) {

        if (bookmarkName == null || bookmarkName.trim().length() == 0) {
            return false;
        }

        synchronized (children) {
            for (int i = 0; i < children.size(); i++) {
                Object o = children.get(i);
                if (o instanceof BookmarkReference) {
                    if (bookmarkName.equals(((BookmarkReference) o).getName())) {
                        return true;
                    }
                }
            }
            return false;
        }
    }
}